import { relative, resolve } from 'path';
import { ArrayMergeMode, deepMerge } from './deep-merge';
import { extractProjectInfo, ProjectBasicInfo, ProjectInfoEnvProps, RuntimeMode } from './project-info';
import { isEmptyString, isExistDir, loadJSON } from './utils';
import baseConfig = require('./base-config');

/**
 * TaroConfig 必要的特性
 */
interface TaroConfigImpl {
  projectName: string;
  date: string;
  sourceRoot: string;
  outputRoot: string;
  defineConstants: Record<string, any>;
  plugins: any[];
  env: Record<string, any>;

  [key: string]: any;
}

type ProjectRootCallbackParams = {
  params: TaroConfigParams,
  info: ProjectBasicInfo,
}

type SourceRootCallbackParams = ProjectRootCallbackParams & {
  projectRoot: string,
}

type OutputPathCallbackParams = ProjectRootCallbackParams & {
  version: string,
}

type TaroConfigParams = {
  root: string,
  // merge?: TaroMergeCallback,
  config?: Record<string, any>,
  projectRoot?: (params: ProjectRootCallbackParams) => string,
  sourceRoot?: (params: SourceRootCallbackParams) => string,
  outputRoot?: (params: SourceRootCallbackParams) => string,
  outputPath?: (params: OutputPathCallbackParams) => string,
}

export type ProjectInfo = ProjectBasicInfo & {
  name?: string,
  version: string,
  projectRoot: string,
  sourceRoot: string,
  outputRoot: string,
  [key: string]: any,
}

type TaroConfigReturn = {
  info: ProjectInfo,
  config: TaroConfigImpl,
}

const getProjectRoot = (params: TaroConfigParams, info: ProjectBasicInfo): string => {
  if (typeof params.projectRoot === 'function') {
    return params.projectRoot({ params, info });
  }
  return resolve(params.root, 'packages', info.project);
};

const getSourceRoot = (params: TaroConfigParams, info: ProjectBasicInfo): string => {
  const projectRoot = getProjectRoot(params, info);
  if (typeof params.sourceRoot === 'function') {
    return params.sourceRoot({ params, info, projectRoot });
  }
  return resolve(projectRoot, 'src');
};

const getOutputRoot = (params: TaroConfigParams, info: ProjectBasicInfo): string => {
  const projectRoot = getProjectRoot(params, info);
  if (typeof params.outputRoot === 'function') {
    return params.outputRoot({ params, info, projectRoot });
  }
  return resolve(params.root, info.mode === RuntimeMode.DEV ? 'development' : 'release');
};

const getOutputPath = (params: TaroConfigParams, info: ProjectBasicInfo, version: string): string => {
  if (typeof params.outputPath === 'function') {
    return params.outputPath({ params, info, version });
  }
  const { project, platform, mode, custom } = info;
  let dir: string[] = [project, platform];
  let slash = '-';
  if (!isEmptyString(custom)) {
    dir.push(custom);
  }
  if (mode === RuntimeMode.PROD) {
    slash = '/';
    dir.push(version);
  }
  return dir.join(slash);
};

/**
 * 加载项目的配置信息，包括解析 npm 指令，加载项目的 package.json 文件
 *
 * @param params
 * @param env
 */
const loadProjectInfo = (
  params: TaroConfigParams,
  env?: ProjectInfoEnvProps,
): ProjectInfo => {
  env = env || process.env;
  const basicInfo = extractProjectInfo(env);
  const { project } = basicInfo;
  if (project == null || project === '') {
    throw new Error(`项目名称不得为空`);
  }

  const projectRoot = getProjectRoot(params, basicInfo);
  if (!isExistDir(projectRoot)) {
    throw new Error(`项目 ${project} 目录不存在`);
  }

  const packageInfo = loadJSON(resolve(projectRoot, 'package.json'));
  if (packageInfo === false) {
    throw new Error(`无法加载 ${project}/package.json 文件，文件不存在或文件解析出错`);
  }
  if (packageInfo.version == null || packageInfo.version === '') {
    throw new Error(`${project}/package.json 文件未设置有效的版本号`);
  }

  const sourceRoot = getSourceRoot(params, basicInfo);
  const outputRoot = resolve(getOutputRoot(params, basicInfo), getOutputPath(params, basicInfo, packageInfo.version));

  return Object.assign({}, basicInfo, packageInfo, {
    projectRoot, sourceRoot, outputRoot,
  }) as ProjectInfo;
};

const getDate = (): string => {
  const date = new Date();
  return `${date.getFullYear()}-${(date.getMonth() + 1)}-${date.getDate()}`;
};

const generateConstants = ({ project, platform, mode, version, custom }: ProjectInfo): Record<string, any> => {
  return {
    AppName    : JSON.stringify(project),
    AppVersion : JSON.stringify(version),
    AppPlatform: JSON.stringify(platform),
    AppMode    : JSON.stringify(mode),
    AppCustom  : JSON.stringify(custom),
  };
};

// const generateEnv = (info: ProjectInfo): Record<string, any> => {
//   return {
//     // NODE_ENV: info.mode === RuntimeMode.DEV ? 'development' : 'production',
//   };
// };

/**
 * 初始化 taroConfig 参数
 *
 * @param params
 */
const initParams = (
  params: Omit<TaroConfigParams, 'root'> & { root?: string },
): TaroConfigParams => {
  // if (typeof params === 'function') {
  //   params = { merge: params } as TaroConfigParams;
  // }
  // if (typeof params.merge !== 'function') {
  //   params.merge = (...args: any[]) => deepMerge(args, { arrayMode: ArrayMergeMode.COMBINE });
  // }

  if (params.root == null || params.root === '') {
    params.root = process.cwd();
  } else if (!isExistDir(params.root)) {
    throw new Error(`无效的 root 目录`);
  }

  return params as TaroConfigParams;
};

// 不用再传入 merge 的句柄
export const taroConfig = (
  inputParams: Omit<TaroConfigParams, 'root'> & { root?: string },
  env?: ProjectInfoEnvProps,
): TaroConfigReturn => {
  const params = initParams(inputParams);
  const info = loadProjectInfo(params, env);

  let config: TaroConfigImpl = Object.assign({}, baseConfig) as TaroConfigImpl;
  if (typeof params.config === 'object') {
    config = deepMerge<TaroConfigImpl>([
      config,
      params.config as TaroConfigImpl,
    ], { arrayMode: ArrayMergeMode.COMBINE });
  }

  process.env.NODE_ENV = info.mode === RuntimeMode.DEV ? 'development' : 'production';

  config.projectName = info.project;
  config.date = getDate();
  config.sourceRoot = relative(params.root, info.sourceRoot);
  config.outputRoot = relative(params.root, info.outputRoot);
  config.defineConstants = Object.assign(config.defineConstants, generateConstants(info));
  // config.env = Object.assign(config.env, generateEnv(info));

  return { config, info };
};

